/*
  Note: coding GUIs in pure C/++/WinAPI is a pain and the
  programmmer writing this program is known not to have the
  best focus and/or work ethic in the world. Therefore, the
  quality of the code in this file will likely be
  significantly lower than that of any other file in this
  project.

  Indeed, this is the most unorganized & unstructured piece
  of monolithic shit you will probably ever see in your
  lifetime. What we're talking about here is procedural
  programming ..without even separate methods.
*/

#include <windows.h>
#include <windowsx.h>
#include <commctrl.h>
#include <string>
#include <map>
#include <algorithm>
#include <boost/lexical_cast.hpp>
#include <boost/foreach.hpp>
#include "Codepad.hpp"
#include "Snippet.hpp"
#include "SnippetSearch.hpp"
#include "CodefolderLogs.hpp"
#include "Resources.h"

using std::string;
using std::map;
using std::copy;

using boost::lexical_cast;

#ifndef foreach
# define foreach BOOST_FOREACH
#endif // foreach

enum {
  IDM_OPEN = 0x6969,
  IDM_NEW,
  IDM_DELETE,
  IDM_SEARCH,
};

enum {
  // background colors
  COLOR_BACKGROUND_REGULAR = RGB(255, 255, 255),
  COLOR_BACKGROUND_TINTED  = RGB(248, 248, 255),

  // text colors
  COLOR_TEXT_REGULAR  = RGB(0, 0, 0),
  COLOR_TEXT_FADED    = RGB(70, 70, 70),
};

enum {
  LANGUAGE_INDEX_C,
  LANGUAGE_INDEX_CPP,
  LANGUAGE_INDEX_D,
  LANGUAGE_INDEX_HASKELL,
  LANGUAGE_INDEX_LUA,
  LANGUAGE_INDEX_OCAML,
  LANGUAGE_INDEX_PHP,
  LANGUAGE_INDEX_PERL,
  LANGUAGE_INDEX_PLAIN,
  LANGUAGE_INDEX_PYTHON,
  LANGUAGE_INDEX_RUBY,
  LANGUAGE_INDEX_SCHEME,
  LANGUAGE_INDEX_TCL,
};

static const string XMLLOGFILE = "logs.xml";

static SnippetList      g_snippetList;
static CodefolderLogs*  g_logs;
static Codepad*         g_codepad;

int MessageBoxWithError(HWND window)
{
  static const string CAPTION = "codefolder - error";
  
  string error    = lexical_cast<string>(GetLastError());
  string message  = "An error has occurred. [" + error + "]";
  
  return MessageBox(window, message.c_str(), CAPTION.c_str(), MB_OK | MB_ICONERROR);
}

// Source/credits: written for XPI by Irwin/x0r
HANDLE AddFontFromResource(__in HMODULE hModule, __in LPTSTR lpcszName)
{
  HRSRC   hResource;
  DWORD   dwFontCount;

  hResource = FindResource(hModule, lpcszName, RT_FONT);
  if (hResource == NULL)
    return NULL;

  HANDLE hRET = NULL;

  HGLOBAL hMemory = LoadResource(hModule, hResource);
  if (hMemory != NULL)
  {
    LPVOID lpData = LockResource(hMemory);
    if (lpData != NULL)
      hRET = AddFontMemResourceEx(lpData, SizeofResource(hModule, hResource), NULL, &dwFontCount);
    FreeResource(hMemory);
  }

  return hRET;
}

HBITMAP BitmapFromIcon(HICON icon, int width, int height)
{
  HBITMAP bitmap;
  bool    success = false;

  HDC context = CreateCompatibleDC(NULL);
  if (context != NULL)
  {
    bitmap = CreateCompatibleBitmap(context, width, height);
    if (bitmap != NULL)
    {
      HBITMAP oldBitmap = reinterpret_cast<HBITMAP>(SelectObject(context, bitmap));
      if (oldBitmap != NULL)
        if (DrawIcon(context, 0, 0, icon))
          if (SelectObject(context, oldBitmap) != NULL)
            success = true;
    }
    DeleteDC(context);
  }
  return (success ? bitmap : NULL);
}

void ListViewAddSnippet(HWND listView, string id, string language, string description)
{
  LVITEM item;

  item.mask = LVIF_TEXT;
  item.iItem = ListView_GetItemCount(listView);

  item.iSubItem = 0;
  item.pszText = const_cast<char*>(id.c_str());
  ListView_InsertItem(listView, &item);

  item.iSubItem = 1;
  item.pszText = const_cast<char*>(language.c_str());
  ListView_SetItem(listView, &item);

  item.iSubItem = 2;
  item.pszText = const_cast<char*>(description.c_str());
  ListView_SetItem(listView, &item);
}

INT_PTR CALLBACK DialogNewProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam)
{
  UNREFERENCED_PARAMETER(lParam);

  static map<int, string> languages;
  static HFONT fontConsolas;
  
  // language, code, description, run
  switch (message)
  {
  case WM_INITDIALOG:
    { 
      // ----------------------------------
      // set edit-box font
      // ----------------------------------

      fontConsolas = reinterpret_cast<HFONT>(lParam);
      SendMessage(GetDlgItem(window, IDC_CODE), WM_SETFONT, reinterpret_cast<WPARAM>(fontConsolas), true);

      // ----------------------------------
      // initialize languages & combo-box
      // ----------------------------------
      
      languages[LANGUAGE_INDEX_C]        = LANGUAGE_C;
      languages[LANGUAGE_INDEX_CPP]      = LANGUAGE_CPP;
      languages[LANGUAGE_INDEX_D]        = LANGUAGE_D;
      languages[LANGUAGE_INDEX_HASKELL]  = LANGUAGE_HASKELL;
      languages[LANGUAGE_INDEX_LUA]      = LANGUAGE_LUA;
      languages[LANGUAGE_INDEX_OCAML]    = LANGUAGE_OCAML;
      languages[LANGUAGE_INDEX_PHP]      = LANGUAGE_PHP;
      languages[LANGUAGE_INDEX_PERL]     = LANGUAGE_PERL;
      languages[LANGUAGE_INDEX_PLAIN]    = LANGUAGE_PLAIN;
      languages[LANGUAGE_INDEX_PYTHON]   = LANGUAGE_PYTHON;
      languages[LANGUAGE_INDEX_RUBY]     = LANGUAGE_RUBY;
      languages[LANGUAGE_INDEX_SCHEME]   = LANGUAGE_SCHEME;
      languages[LANGUAGE_INDEX_TCL]      = LANGUAGE_TCL;
      
      HWND comboBox = GetDlgItem(window, IDC_LANGUAGES);
      
      ComboBox_AddString(comboBox, LANGUAGE_C.c_str());
      ComboBox_AddString(comboBox, LANGUAGE_CPP.c_str());
      ComboBox_AddString(comboBox, LANGUAGE_D.c_str());
      ComboBox_AddString(comboBox, LANGUAGE_HASKELL.c_str());
      ComboBox_AddString(comboBox, LANGUAGE_LUA.c_str());
      ComboBox_AddString(comboBox, LANGUAGE_OCAML.c_str());
      ComboBox_AddString(comboBox, LANGUAGE_PHP.c_str());
      ComboBox_AddString(comboBox, LANGUAGE_PERL.c_str());
      ComboBox_AddString(comboBox, LANGUAGE_PLAIN.c_str());
      ComboBox_AddString(comboBox, LANGUAGE_PYTHON.c_str());
      ComboBox_AddString(comboBox, LANGUAGE_RUBY.c_str());
      ComboBox_AddString(comboBox, LANGUAGE_SCHEME.c_str());
      ComboBox_AddString(comboBox, LANGUAGE_TCL.c_str());
    }
    break;
    
  case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case IDC_CREATE:
      {
        int   languageSelected;
        int   descriptionLength, codeLength;
        char* descriptionBuffer, * codeBuffer;
        bool  run;
        
        // #1 - LANGUAGE
        languageSelected = ComboBox_GetCurSel(GetDlgItem(window, IDC_LANGUAGES));
        
        // #2 - DESCRIPTION
        descriptionLength = GetWindowTextLength(GetDlgItem(window, IDC_DESCRIPTION));
        if (descriptionLength == 0)
        {
          MessageBox(window, "You must fill in description.", "codefolder - error", MB_OK | MB_ICONERROR);
          break;
        }
        
        descriptionBuffer = new char[descriptionLength + 1];
        if (GetDlgItemText(window, IDC_DESCRIPTION, descriptionBuffer, descriptionLength + 1) == 0)
        {
          MessageBoxWithError(window);
          break;
        }
        
        // #3 - CODE
        codeLength = GetWindowTextLength(GetDlgItem(window, IDC_CODE));
        if (codeLength == 0)
        {
          MessageBoxWithError(window);
          break;
        }
        
        codeBuffer = new char[codeLength + 1];
        if (GetDlgItemText(window, IDC_CODE, codeBuffer, codeLength + 1) == 0)
        {
          MessageBoxWithError(window);
          break;
        }
        
        // #4 - RUN OR NOT
        run = (IsDlgButtonChecked(window, IDC_RUN) == BST_CHECKED);
        
        // #5 - CREATE SNIPPET
        Snippet*  snippet = g_codepad->CreateSnippet(codeBuffer, languages[languageSelected], descriptionBuffer, run);
        HWND      parent = GetParent(window);
        HWND      listView = GetDlgItem(parent, IDC_SNIPPETS);

        g_snippetList.push_back(snippet);
        ListViewAddSnippet(listView, snippet->m_snippetId, snippet->m_language, snippet->m_description);

        EndDialog(window, 0);
      }
      break;
    }
    break;
    
  case WM_CLOSE:
    EndDialog(window, 0);
    break;
  }
  return static_cast<INT_PTR>(false);
}

LRESULT CALLBACK EditBoxProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam)
{
  static WNDPROC oldWndProc = reinterpret_cast<WNDPROC>(GetWindowLongPtr(window, GWLP_USERDATA));

  switch (message)
  {
  case WM_GETDLGCODE:
    return (DLGC_WANTALLKEYS | CallWindowProc(oldWndProc, window, message, wParam, lParam));

  case WM_CHAR:
    {
      bool processed = false;

      if (wParam == VK_RETURN)
      {
        SendMessage(GetParent(window), WM_COMMAND, MAKEWPARAM(IDC_SEARCH, 0),
          reinterpret_cast<LPARAM>(GetDlgItem(GetParent(window), IDC_SEARCH)));
        processed = true;
      }

      if (wParam == VK_ESCAPE)
      {
        SendMessage(GetParent(window), WM_KEYDOWN, VK_ESCAPE, lParam);
        processed = true;
      }

      if (processed)
        return 0;
    }
    break;
  }
  return CallWindowProc(oldWndProc, window, message, wParam, lParam);
}

INT_PTR CALLBACK DialogSearchProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam)
{ 
  UNREFERENCED_PARAMETER(lParam);

  switch (message)
  {
  case WM_INITDIALOG:
    {
      HWND editBox = GetDlgItem(window, IDC_QUERY);

      SetFocus(editBox);

      WNDPROC oldWndProc = reinterpret_cast<WNDPROC>(GetWindowLongPtr(editBox, GWLP_WNDPROC));

      SetWindowLongPtr(editBox, GWLP_USERDATA, reinterpret_cast<LONG>(oldWndProc));
      SetWindowLongPtr(editBox, GWLP_WNDPROC, reinterpret_cast<LONG>(EditBoxProc));
    }
    break;

  case WM_KEYDOWN:
    if (wParam == VK_ESCAPE)
      SendMessage(window, WM_CLOSE, 0, 0);
    break;

  case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case IDC_SEARCH:
      {
        SnippetList tempSnippetList(g_snippetList);

        int searchLength = GetWindowTextLength(GetDlgItem(window, IDC_QUERY));
        if (searchLength == 0)
        {
          HWND listView = GetDlgItem(GetParent(window), IDC_SNIPPETS);

          ListView_DeleteAllItems(listView);
          foreach (Snippet* snippet, g_snippetList) {
            ListViewAddSnippet(listView, snippet->m_snippetId, snippet->m_language, snippet->m_description);
          }

          break;
        }
          
        char* search = new char[searchLength + 1];

        if (GetDlgItemText(window, IDC_QUERY, search, searchLength + 1) == 0)
        {
          MessageBoxWithError(window);
          break;
        }
        
        HWND listView = GetDlgItem(GetParent(window), IDC_SNIPPETS);

        SnippetList newList;
        SnippetSearch::SearchQuery(search, tempSnippetList);

        ListView_DeleteAllItems(listView);
        foreach (Snippet* snippet, tempSnippetList) {
          ListViewAddSnippet(listView, snippet->m_snippetId, snippet->m_language, snippet->m_description);
        }
      }
      break;
    }
    break;

  case WM_CLOSE:
    {
      HWND listView = GetDlgItem(GetParent(window), IDC_SNIPPETS);

      ListView_DeleteAllItems(listView);
      foreach (Snippet* snippet, g_snippetList) {
        ListViewAddSnippet(listView, snippet->m_snippetId, snippet->m_language, snippet->m_description);
      }

      EndDialog(window, 0);
    }
    break;
  }
  return static_cast<INT_PTR>(false);
}

LRESULT CALLBACK ListViewProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam)
{
  static WNDPROC oldWndProc = reinterpret_cast<WNDPROC>(GetWindowLongPtr(window, GWLP_USERDATA));

  switch (message)
  {
  case WM_KEYDOWN:
    if (wParam == 'F')
      SendMessage(GetParent(window), WM_KEYDOWN, wParam, lParam);
    break;

  case WM_CONTEXTMENU:
    {
      HMENU menu;
      POINT point;

      menu = CreatePopupMenu();
      if (menu != NULL)
      {
        HWND parent = GetParent(window);
        bool selected = (ListView_GetSelectionMark(window) != -1);
        
        // TODO: use icons
        InsertMenu(menu, 0, MF_BYPOSITION | (selected ? MF_STRING : MF_STRING | MF_DISABLED), IDM_OPEN, TEXT("Open"));
        InsertMenu(menu, 1, MF_BYPOSITION | MF_SEPARATOR, 0, NULL);
        InsertMenu(menu, 2, MF_BYPOSITION | MF_STRING, IDM_NEW, TEXT("New"));
        InsertMenu(menu, 3, MF_BYPOSITION | (selected ? MF_STRING : MF_STRING | MF_DISABLED), IDM_DELETE, TEXT("Delete"));
        InsertMenu(menu, 4, MF_BYPOSITION | MF_SEPARATOR, 0, NULL);
        InsertMenu(menu, 5, MF_BYPOSITION | MF_STRING, IDM_SEARCH, TEXT("Search.."));

        if (SetForegroundWindow(parent))
          if (GetCursorPos(&point))
            TrackPopupMenu(menu, TPM_TOPALIGN | TPM_LEFTALIGN, point.x, point.y, 0, parent, NULL);

        DestroyMenu(menu);
      }
    }
    break;

  case WM_LBUTTONDBLCLK:
    {
      int     selection = ListView_GetSelectionMark(window);
      string  url = "http://codepad.org/" + g_snippetList.at(selection)->m_snippetId;
    
      ShellExecute(NULL, "open", url.c_str(), NULL, NULL, SW_NORMAL);
    }
    break;
  }
  return CallWindowProc(oldWndProc, window, message, wParam, lParam);
}

INT_PTR CALLBACK WindowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam)
{
  UNREFERENCED_PARAMETER(lParam);
  
  static HANDLE fontResourceConsolas, fontResourceSegoeUI;
  static HFONT  fontHandleConsolas, fontHandleSegoeUI;
  static HICON  iconSmall, iconLarge;

  switch (message)
  {
  case WM_INITDIALOG:
    {
      HWND listView = GetDlgItem(window, IDC_SNIPPETS);
      
      // ----------------------------
      // set window icon
      // ----------------------------
      
      iconSmall = reinterpret_cast<HICON>(LoadImage(GetModuleHandle(NULL), MAKEINTRESOURCE(IDI_CODEFOLDER), IMAGE_ICON, 16, 16, LR_DEFAULTCOLOR));
      if (iconSmall != NULL)
        SendMessage(window, WM_SETICON, ICON_SMALL, reinterpret_cast<LPARAM>(iconSmall));

      iconLarge = reinterpret_cast<HICON>(LoadImage(GetModuleHandle(NULL), MAKEINTRESOURCE(IDI_CODEFOLDER), IMAGE_ICON, 0, 0, LR_DEFAULTCOLOR | LR_DEFAULTSIZE));
      if (iconLarge != NULL)
        SendMessage(window, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(iconLarge));

      // ----------------------------
      // subclass list-view
      // ----------------------------

      WNDPROC oldWndProc = reinterpret_cast<WNDPROC>(GetWindowLongPtr(listView, GWLP_WNDPROC));

      SetWindowLongPtr(listView, GWLP_USERDATA, reinterpret_cast<LONG>(oldWndProc));
      SetWindowLongPtr(listView, GWLP_WNDPROC, reinterpret_cast<LONG>(ListViewProc));

      // ----------------------------
      // initialize list-view columns
      // ----------------------------

      LVCOLUMN column;
      RECT size;

      if (!GetWindowRect(listView, &size))
      {
        EndDialog(window, 0);
        break;
      }

      column.mask = LVCF_WIDTH | LVCF_TEXT;
      
      column.cx = 80;
      column.pszText = "codepad ID";
      ListView_InsertColumn(listView, 0, &column);

      column.cx = 80;
      column.pszText = "Language";
      ListView_InsertColumn(listView, 1, &column);

      column.cx = size.right - size.left - 80 - 80;
      column.pszText = "Description";
      ListView_InsertColumn(listView, 2, &column);

      // ----------------------------
      // apply extra list-view styles
      // ----------------------------

      ListView_SetExtendedListViewStyle(listView,
        LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER | LVS_EX_FULLROWSELECT);

      // ----------------------------
      // disable header
      // ----------------------------

      EnableWindow(ListView_GetHeader(listView), static_cast<BOOL>(false));
      
      // ----------------------------
      // initialize Consolas font
      // ----------------------------

      fontResourceConsolas  = AddFontFromResource(GetModuleHandle(NULL), MAKEINTRESOURCE(IDR_CONSOLAS));
      fontResourceSegoeUI   = AddFontFromResource(GetModuleHandle(NULL), MAKEINTRESOURCE(IDR_SEGOEUI));
      fontHandleConsolas    = CreateFont(-12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, "Consolas");
      fontHandleSegoeUI     = CreateFont(-12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, "Segoe UI");

      // ----------------------------
      // add list-view items
      // ----------------------------

      g_logs->Fetch(g_snippetList);
      foreach (Snippet* snippet, g_snippetList) {
        ListViewAddSnippet(listView, snippet->m_snippetId, snippet->m_language, snippet->m_description);
      }
    }
    break;
    
  case WM_KEYDOWN:
    if (wParam == 'F')
      if (GetAsyncKeyState(VK_CONTROL) & 0x8000)
        SendMessage(window, WM_COMMAND, MAKEWPARAM(IDM_SEARCH, 0), 0);
    break;

  case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case IDM_OPEN:
      {
        int     selection = ListView_GetSelectionMark(GetDlgItem(window, IDC_SNIPPETS));
        string  url = "http://codepad.org/" + g_snippetList.at(selection)->m_snippetId;
    
        ShellExecute(NULL, "open", url.c_str(), NULL, NULL, SW_NORMAL);
      }
      break;

    case IDM_NEW:
      DialogBoxParam(GetModuleHandle(NULL), MAKEINTRESOURCE(IDD_NEW), window, DialogNewProc, reinterpret_cast<LPARAM>(fontHandleConsolas));
      break;
      
    case IDM_DELETE:
      {
        HWND  listView = GetDlgItem(window, IDC_SNIPPETS);
        int   selection = ListView_GetSelectionMark(listView);
    
        // no selection
        if (selection == -1)
          break;
          
        // erase from snippet collection & list-view
        g_snippetList.erase(g_snippetList.begin() + selection);
        ListView_DeleteItem(listView, selection);
      }
      break;

    case IDM_SEARCH:
      DialogBox(GetModuleHandle(NULL), MAKEINTRESOURCE(IDD_SEARCH), window, DialogSearchProc);
      break;
    }
    break;

  case WM_NOTIFY:
    {
      if (reinterpret_cast<LPNMLISTVIEW>(lParam)->hdr.code != NM_CUSTOMDRAW)
        break;

      LPNMLVCUSTOMDRAW  customDraw = reinterpret_cast<LPNMLVCUSTOMDRAW>(lParam);
      HWND              listView = GetDlgItem(window, IDC_SNIPPETS);
      LRESULT           ret = CDRF_DODEFAULT;

      switch (customDraw->nmcd.dwDrawStage)
      {
      case CDDS_PREPAINT:
        ret = CDRF_NOTIFYITEMDRAW;
        break;
        
      case CDDS_ITEMPREPAINT:
        {
          RECT rect;
          if (ListView_GetSubItemRect(listView, customDraw->nmcd.dwItemSpec, customDraw->iSubItem, LVIR_BOUNDS, &rect))
          {
            bool which = (customDraw->nmcd.dwItemSpec % 2 == 0);

            customDraw->clrTextBk = (which ? COLOR_BACKGROUND_REGULAR : COLOR_BACKGROUND_TINTED);
          }

          ret = CDRF_NOTIFYSUBITEMDRAW | CDRF_NEWFONT;
        }
        break;

      case (CDDS_ITEMPREPAINT | CDDS_SUBITEM):
        {
          bool which = (customDraw->iSubItem == 0);

          customDraw->clrText = (which ? COLOR_TEXT_FADED : COLOR_TEXT_REGULAR);
          SelectObject(customDraw->nmcd.hdc, (which ? fontHandleConsolas : fontHandleSegoeUI));

          ret = CDRF_NEWFONT;
        }
        break;
      }
      SetWindowLongPtr(window, DWLP_MSGRESULT, ret);
      return static_cast<LRESULT>(true);
    }
    break;

  case WM_CLOSE:
    if (fontResourceConsolas != NULL)
      RemoveFontMemResourceEx(fontResourceConsolas);

    if (fontResourceSegoeUI != NULL)
      RemoveFontMemResourceEx(fontResourceSegoeUI);

    if (fontHandleConsolas != NULL)
      DeleteObject(fontHandleConsolas);

    if (fontHandleSegoeUI != NULL)
      DeleteObject(fontHandleSegoeUI);

    if (iconSmall != NULL)
      DestroyIcon(iconSmall);

    if (iconLarge != NULL)
      DestroyIcon(iconLarge);

    g_logs->Save(g_snippetList);

    EndDialog(window, 0);
    break;
  }
  return static_cast<LRESULT>(false);
}

int WINAPI WinMain(HINSTANCE instance, HINSTANCE prev, char* commandLine, int showCommand)
{
  UNREFERENCED_PARAMETER(prev);
  UNREFERENCED_PARAMETER(commandLine);
  UNREFERENCED_PARAMETER(showCommand);

  g_logs    = new CodefolderLogs(XMLLOGFILE);
  g_codepad = new Codepad();

  if (!g_logs->LoadSuccess())
  {
    // TODO: handle this error
    MessageBoxWithError(NULL);
    return EXIT_FAILURE;
  }

  DialogBox(instance, MAKEINTRESOURCE(IDD_MAIN), NULL, WindowProc);

  delete g_logs;
  delete g_codepad;

  return EXIT_SUCCESS;
}